Skip to content

[utils] Add opt-in DataAttributesOverrides augmentation for slot props#48554

Open
LukasTy wants to merge 5 commits into
mui:masterfrom
LukasTy:claude/data-attributes-overrides
Open

[utils] Add opt-in DataAttributesOverrides augmentation for slot props#48554
LukasTy wants to merge 5 commits into
mui:masterfrom
LukasTy:claude/data-attributes-overrides

Conversation

@LukasTy
Copy link
Copy Markdown
Member

@LukasTy LukasTy commented May 20, 2026

Created based on mui/mui-x#22128 exploration.

Closes #33175.

Summary

Adds a single opt-in switch — DataAttributesOverrides — that lets consumers declare typed support for data-* attributes on every MUI slot prop. Augmenting the interface is the one sanctioned way to flip the level of strictness; nothing is widened by default.

Today, code like

<Backdrop slotProps={{ root: { 'data-testid': 'backdrop' } }} />

is a TypeScript error even though the attribute is forwarded to the DOM at runtime. After this PR, consumers can opt in with a one-time augmentation:

declare module '@mui/utils/types' {
  interface DataAttributesOverrides {
    'data-testid'?: string;
  }
}

Or, for the loose / "anything goes" form:

declare module '@mui/utils/types' {
  interface DataAttributesOverrides {
    [k: `data-${string}`]: string | number | boolean | undefined;
  }
}

After augmentation, every Material component that wires slot props through SlotComponentProps / SlotComponentPropsWithSlotState (or through SlotProps in @mui/material) picks up the augmented keys automatically.

Changes

@mui/utils/types

  • New DataAttributes.ts:
    • DataAttributesOverrides — empty, module-augmentable interface. The single switch consumers flip.
    • DataAttributes = DataAttributesOverrides — dormant by default; activates when augmented.
    • WithDataAttributes<T> = T | (T & DataAttributes) — union form so the original T stays assignable as-is (preserves backwards compatibility with x as CustomProps style casts on slot values), while augmented keys flow through the widened branch when consumers opt in.
  • index.ts re-exports the new symbols, and wraps both SlotComponentProps and SlotComponentPropsWithSlotState (object branch and callback branch) with WithDataAttributes.

Module-augmentation test

packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverrides.{spec.tsx,tsconfig.json} augments @mui/utils/types and uses Backdrop's root slot to verify the augmentation flows through SlotPropsSlotComponentPropsWithDataAttributes.

Notes for reviewers

Why a union, not an intersection

export type SlotComponentProps<...> =
  | WithDataAttributes<Partial<React.ComponentPropsWithRef<TSlotComponent>> & TOverrides>
  | ((ownerState: TOwnerState) =>
      WithDataAttributes<Partial<React.ComponentPropsWithRef<TSlotComponent>> & TOverrides>);

export type WithDataAttributes<T> = T | (T & DataAttributes);

Only the widened variant carries DataAttributes. This preserves backwards compatibility:

  • { id: 'foo' } as CustomLabelProps assigns to the narrow variant, so CustomLabelProps does not need to declare a data-${string} index signature.
  • { 'data-testid': 'x' } (after augmentation) assigns to the widened variant.
  • The callback branch also uses WithDataAttributes, so consumers returning someObj as CustomProps from a slot callback stay assignable.

Why opt-in (no default widening)

Slot prop types should not silently accept arbitrary data-* keys — that hides typos, makes the surface less discoverable in hover/autocomplete, and disagrees with the principle that React's typed surface is what consumers see.

The augmentation hook is a single, well-known module path (@mui/utils/types) that consumers can shape to whichever level they want: strict (one named key), loose (full data-* template-literal index signature), or anywhere in between.

Why @mui/utils/types is the right home

Every MUI slot prop type ultimately flows through SlotComponentProps / SlotComponentPropsWithSlotState in @mui/utils/types. Putting the augmentation hook here means:

  • A consumer's single augmentation lights up every Material component's slots automatically.
  • Downstream packages (@mui/x-data-grid, @mui/x-date-pickers, @mui/x-charts, ...) inherit the augmentation transitively — they don't need to mirror the helper themselves.
  • The contract is documented in one place.

Test plan

  • pnpm --filter "@mui/utils" run typescript passes.
  • pnpm --filter "@mui/material" run typescript passes.
  • pnpm typescript:module-augmentation — all 21+ existing tests pass, plus the new one.
  • pnpm prettier --check and pnpm eslint clean on the changed files.
  • Manual: with the four-line DataAttributesOverrides augmentation above, <Backdrop slotProps={{ root: { 'data-testid': 'foo' } }} /> type-checks and forwards to the DOM as expected. Without the augmentation, the same code is a TS error (matches today).

Today, passing `data-testid` (or any `data-*` attribute) through `slotProps`
on a MUI component is a TypeScript error even though the attribute is
forwarded to the DOM at runtime. This adds a single, opt-in switch that
lets consumers declare exactly which `data-*` keys they want typed, at
whichever level of strictness they choose.

- `DataAttributesOverrides`: module-augmentable empty interface in
  `@mui/utils/types`. The single sanctioned switch.
- `DataAttributes = DataAttributesOverrides`: dormant by default;
  populated only when a consumer augments.
- `WithDataAttributes<T> = T | (T & DataAttributes)`: union form so the
  original `T` stays assignable as-is (preserves backwards compatibility
  with `x as CustomProps` style casts on slot values), while augmented
  keys flow through the widened branch when consumers opt in.
- `SlotComponentProps` and `SlotComponentPropsWithSlotState` now wrap
  their object and callback branches with `WithDataAttributes`. With an
  empty default the wrapping is a no-op until a consumer augments — once
  they do, every Material component that reaches slot props through these
  helpers (or through `SlotProps` in `@mui/material`) picks up the new
  keys automatically.

Consumers opt in with a single `declare module '@mui/utils/types' { ... }`
block. Examples are documented in the new `DataAttributes.ts` file and
exercised by a module-augmentation test using Backdrop's root slot.

This is the canonical place for the helper because every MUI slot prop
type ultimately flows through `@mui/utils/types`; downstream packages
(`@mui/x-*` and friends) get the augmentation transitively without
having to mirror the helper themselves.
@code-infra-dashboard
Copy link
Copy Markdown

code-infra-dashboard Bot commented May 20, 2026

Deploy preview

https://deploy-preview-48554--material-ui.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/material 0B(0.00%) 0B(0.00%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@zannager zannager added the package: utils Specific to the utils package. label May 20, 2026
@zannager zannager requested a review from mnajdova May 20, 2026 13:23
Comment thread packages/mui-utils/src/types/DataAttributes.ts Outdated
Comment thread packages/mui-utils/src/types/DataAttributes.ts Outdated
@LukasTy LukasTy added the type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. label May 20, 2026
@mnajdova
Copy link
Copy Markdown
Member

Or, for the loose / "anything goes" form:

declare module '@mui/utils/types' {
  interface DataAttributesOverrides {
    [k: `data-${string}`]: string | number | boolean | undefined;
  }
}

I would go with this option if we do decide to do something about it, as it reflects the primitives elements. Can you link some issues related to the problem? What are people typically complaining about.

@LukasTy LukasTy self-assigned this Jun 3, 2026
@LukasTy
Copy link
Copy Markdown
Member Author

LukasTy commented Jun 3, 2026

Can you link some issues related to the problem? What are people typically complaining about.
Thanks for taking a look. Issue links and a summary of what people hit below.

Umbrella issues

More specific reports (same root cause)

What people are actually complaining about

  1. Testing locators, overwhelmingly. Almost every report is someone trying to attach data-testid / data-cy / data-test-id for Playwright, Cypress, or Testing Library and getting a TS error.
  2. It is purely a typings problem. The attributes are forwarded to the DOM at runtime today; only the compile-time check rejects them. [textfield] Typings for formHelperText slot missing data attributes #47230 says this outright ("this is purely a typings issue. At run-time it works perfectly").
  3. The inconsistency is the sharpest pain. The same component accepts data-* on one slot and rejects it on another (TextField htmlInput vs formHelperText, Checkbox inputProps, Badge badge slot), so people cannot predict where the cast is needed.
  4. The current workarounds are unsatisfying. Either a global declare module 'react' augmentation of HTMLAttributes (too broad, leaks everywhere), or as any at each call site. The top-voted comment on [core] Allow data-* attributes on all *props  #33175 is exactly the global-augmentation hack, and Bump @types/uuid from 8.0.0 to 8.0.1 #22126 shows the hand-rolled CustomInputProps extends React.InputHTMLAttributes pattern.

On the "anything goes" form

Agreed, and the opt-in design already supports it as the headline example:

declare module '@mui/utils/types' {
  interface DataAttributesOverrides {
    [k: `data-${string}`]: string | number | boolean | undefined;
  }
}

That single augmentation lights up every slot of every Material (and downstream X) component at once, which directly answers the "I just want data-* everywhere, like the primitive elements" use case from #33175 and #22126. The strongly-typed form (one named key) stays available for teams that want a closed allow-list, but I am happy to make the index-signature form the primary documented example since it matches what most of these reports are asking for.

One drawback of this approach is no IntelliSense. With specific typing, users would get autocompletion for defined fields.
I think we should mention this and have both examples in docs.

LukasTy added 3 commits June 3, 2026 16:42
- DataAttributes.ts: convert the augmentation examples to JSDoc `@example`
  tags so they surface in IDE quick-info. Lead with the loose
  index-signature form, which mirrors the primitive elements and matches
  what most reports ask for.
- dataAttributesOverrides.spec.tsx: switch the primary spec to the loose
  `[k: \`data-${string}\`]` form and exercise both the object and callback
  branches of the slot-prop union.
- dataAttributesOverridesAllowList.spec.tsx (new): cover the strict,
  closed-allow-list form, with a `@ts-expect-error` proving an undeclared
  `data-*` key is still rejected.
- DataAttributes.ts: note in the JSDoc that the loose index-signature form
  accepts any `data-*` key but gives no autocomplete, while the
  strongly-typed form restricts to declared keys in exchange for
  IntelliSense and typo-checking.
- Cross-reference the loose and narrow module-augmentation specs so the
  pair is discoverable from either file, and spell out why they must live
  in separate compilation units (augmentation is global, so the loose
  index signature would otherwise swallow the narrow allow-list).
Replace the dense, semicolon-spliced tradeoff sentence and the
triple-clause @example labels with a scannable two-bullet "which form to
pick" list. The bullets carry the loose-vs-explicit tradeoff (autocomplete
and typo-checking vs accept-anything), so the @example comments shrink to
plain labels.
@LukasTy LukasTy requested a review from mj12albert June 4, 2026 14:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: utils Specific to the utils package. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[core] Allow data-* attributes on all *props

4 participants